from heapq import heappush, heappop, heapify
import math, random
import parole
import main, ai, interface, language, sim, sim_items
from sim import Event, Obj
from util import *
from parole.colornames import colors

#=========================================

class RedBlood(sim.Puddle):
    """
    "Normal" red blood of most mammals, humanoids, etc.
    """
    def __init__(self, amount, big=False):
        char = random.choice(['.', ',', ';'])
        fg_rgb = colors[random.choice(['Red', 'DarkRed', 'FireBrick', 'Crimson',
            'Maroon'])]
        bg_rgb = None
        if big:
            name = random.choice(['puddle', 'pool', 'splash'])
            char = random.choice([char, ' '])
            while (not bg_rgb) or bg_rgb == fg_rgb:
                bg_rgb = colors[random.choice(['DarkRed', 'Maroon'])]
        else:
            name = random.choice(['splatter', 'spattering', 'smear', 'streak',
                'smattering'])

        super(RedBlood, self).__init__(amount, name + ' of blood', 'blood', char,
                fg_rgb, bg_rgb)
        self.big = big

class GreenBlood(sim.Puddle):
    """
    Green blood for monstrous insects.
    """
    def __init__(self, amount, big=False):
        char = random.choice(['.', ',', ';'])
        fg_rgb = colors[random.choice(['Chartreuse', 'DarkGreen', 'DarkOliveGreen', 'Crimson',
            'DarkGoldenRod'])]
        bg_rgb = None
        if big:
            name = random.choice(['puddle', 'pool', 'splash'])
            char = random.choice([char, ' '])
            while (not bg_rgb) or bg_rgb == fg_rgb:
                bg_rgb = colors[random.choice(['DarkGreen', 'ForestGreen'])]
        else:
            name = random.choice(['splatter', 'spattering', 'smear', 'streak',
                'smattering'])

        super(GreenBlood, self).__init__(amount, name + ' of putrid ichor',
                'ichor', char, fg_rgb, bg_rgb)
        self.big = big
        
class BlackBlood(sim.Puddle):
    """
    Black blood for evil and dark-themed monsters.
    """
    def __init__(self, amount, big=False):
        char = random.choice(['.', ',', ';'])
        #fg_rgb = colors[random.choice(['Red', 'DarkRed', 'FireBrick', 'Crimson',
        #    'Maroon'])]
        fg_rgb = (0,0,0)
        bg_rgb = None
        if big:
            name = random.choice(['puddle', 'pool', 'splash'])
            char = ' '
            bg_rgb = (0,0,0)
        else:
            name = random.choice(['splatter', 'spattering', 'smear', 'streak',
                'smattering'])

        super(BlackBlood, self).__init__(amount, name + ' of black ichor',
                'ichor', char, fg_rgb, bg_rgb)
        self.big = big
        
class BiolumBlood(sim.Puddle):
    """
    Bioluminescent blood.
    """
    bloodColors = ['Lime', 'MediumSeaGreen', 'LightSeaGreen', 'Turquoise']

    def __init__(self, amount, big=False):
        char = random.choice(['.', ',', ';'])
        fg_rgb = colors[random.choice(self.bloodColors)]
        bg_rgb = None
        if big:
            name = random.choice(['puddle', 'pool', 'splash'])
            char = random.choice([char, ' '])
            while (not bg_rgb) or bg_rgb == fg_rgb:
                bg_rgb = colors[random.choice(self.bloodColors)]
        else:
            name = random.choice(['splatter', 'spattering', 'smear', 'streak',
                'smattering'])

        super(BiolumBlood, self).__init__(amount, name + ' of luminous fluid',
                'luminous fluid', char, fg_rgb, bg_rgb)
        self.big = big
        brightness = 0.3 if big else 0.1
        self.lightSource = parole.map.LightSource(fg_rgb, brightness)

    def handle_enter_tile(self, event):
        obj, pos, map = event.args
        if obj is not self:
            return
        self.lightSource.apply(map, pos)
        map.update()
        # FIXME: this should happen automatically:
        if main.mapFrame.fovObj and not main.player.dead:
            main.mapFrame.touchFOV()

    def handle_leave_tile(self, event):
        obj, pos = event.args
        if obj is not self:
            return
        self.lightSource.remove(self.parentTile.map)
        self.parentTile.map.update()
        if main.mapFrame.fovObj and not main.player.dead:
            main.mapFrame.touchFOV()

#=========================================

class Creature(sim.Obj):
    """
    A mobile character/creature. Has stats like HP, etc.
    """
    # Default slots available for worn items
    defaultWearSlots = ['body', 'head', 'arms', 'legs', 'feet', 'hands',
                        'weapon', 'shield' ]

    statNames = [
        'hp', 'xp', 'level',

        'maxHP', 'strength', 'dexterity', 'constitution', 'perception', 'speed',
        'stamina', 'maxStamina', 'staminaRegen', 'capacity', 'pv', 'ev', 'hit',
        'damageSkew', 'maxUnarmedDamage', 'minVisibleLight',

        'maxHPMod', 'strengthMod', 'dexterityMod', 'constitutionMod',
        'perceptionMod', 'speedMod', 'maxStaminaMod', 'staminaRegenMod',
        'capacityMod', 'pvMod', 'evMod', 'hitMod', 'damageSkewMod',
        'maxUnarmedDamageMod', 'minVisibleLightMod', ]

    statChanges = {
        # stat -> (increased, decreased)
        'strength': ('stronger', 'weaker'),
        'dexterity': ('more dextrous', 'less dextrous'),
        'constitution': ('heartier', 'less hearty'),
        'perception': ('more perceptive', 'less perceptive'),
        'speed': ('faster', 'slower'),
    }

    # Base energy costs of actions
    walkEnergy = 1000
    toggleDoorEnergy = 2000
    pickupBaseEnergy = 2000 # energy to pickup = base + ceof*weight
    pickupWeightCoefEnergy = 100
    dropEnergy = 500
    unarmedAttackEnergy = 1000
    damageEnergyPerHP = 24000
    receiveAttackEnergy = 24000*5

    energyPerStaminaLoss = 7000 # doing this much work reduces stamina by 1
    regenLossTime = 15*60000 # every this many ms, the stamina regen rate
                             # decreases by 1 (to a minimum of zero)

    windedPercent = 0.70
    fatiguedPercent = 0.40
    exhaustedPercent = 0.10

    def __init__(self, name, shader, strength, dexterity, constitution,
            perception, speed, level, startsVowel=False, wearslots=None,
            unarmedVerb=None, dieVerb='die', leavesCorpse=True,
            unarmedAttackEnergy=None, description=None, bloodClass=RedBlood,
            corporeal=True, feelsPain=True, numHands=2, getsTired=True):
        sim.Obj.__init__(self, name, 10000, shader, startsVowel=startsVowel,
                blockMove=True, blocksNonCorporeal=True)

        self.receivesProjectiles = True
        self.receivesDamage = True
        self.description = description
        self.dead = False
        self.blind = False # not actually blindness, just inability to see own
                           # tile
        self.dieVerb = dieVerb
        self.leavesCorpse = leavesCorpse
        self.lastDamageSource = None
        if unarmedAttackEnergy:
            self.unarmedAttackEnergy = unarmedAttackEnergy
        self.energyToStaminaLoss = self.energyPerStaminaLoss

        # stat base values
        self.strength = strength
        self.dexterity = dexterity
        self.constitution = constitution
        self.perception = perception
        self.speed = speed
        self.level = level
        self.xp = 0

        # stat modifiers
        self.maxHPMod = 0
        self.strengthMod = 0
        self.dexterityMod = 0
        self.constitutionMod = 0
        self.perceptionMod = 0
        self.speedMod = 0

        self.maxHPMod = 0
        self.maxStaminaMod = 0
        self.staminaRegenMod = 0
        self.capacityMod = 0
        self.pvMod = 0
        self.evMod = 0
        self.hitMod = 0
        self.damageSkewMod = 0
        self.maxUnarmedDamageMod = 0
        self.minVisibleLightMod = 0

        # variable and derived stats
        self.setDerivedStatBases()

        # dictionary for keeping track of active status effects other than
        # stat-mods (e.g., farsight, faerie fire, etc.)
        self.statusEffects = {} # string: bool/int/whatever

        # hp and stamina start at max
        self.hp = self.maxHP
        self.stamina = self.maxStamina

        # whether we're in melee, and if so, who we're attacking
        self.meleeTarget = None

        # inventory and slots
        self.inventory = []
        wearslots = wearslots or self.defaultWearSlots
        self.wearSlots = dict([(ws, None) for ws in wearslots])

        # verb used for unarmed melee attacks
        # i would prefer punch, but that's harder to conjugate (epenthetic "e")
        self.unarmedVerb = unarmedVerb or 'sock'

        self.energyLastAction = 0
        self.timeLastAction = 0

        self.bloodClass = bloodClass
        self.corporeal = corporeal
        self.feelsPain = feelsPain
        self.numHands = numHands
        self.__handsFree = numHands
        self.staminaLevel = None # verus 'winded', 'fatigued', 'exhausted'
        self.getsTired = getsTired

        self.pendingEvents = []

        # setup stamina regeneration
        self.scheduleStaminaRegen()
        main.schedule.add(Event('decrease stat', 
                main.schedule.time + self.regenLossTime, 
                (self, 'staminaRegenMod', 1), dests=[self]))


    def nounAnnotation(self):
        hpPcnt = float(self['hp']) / float(self['maxHP'])
        status = None
        if hpPcnt == 1.0:
            status = '{\\Lime uninjured}'
        elif hpPcnt > 0.8:
            status = '{\\Green lightly injured}'
        elif hpPcnt > 0.6:
            status = '{\\Yellow moderately injured}'
        elif hpPcnt > 0.4:
            status = '{\\Orange quite injured}'
        elif hpPcnt > 0.2:
            status = '{\\Red gravely injured}'
        else:
            status = '{\\Violet deathly injured}'

        if self.staminaLevel:
            status += (', %s' % self.staminaLevelColorized())

        return '%s (%s)' % (super(Creature, self).nounAnnotation(),
                            status)

    def setDerivedStatBases(self):
        self.maxHP = int(round(75*self['constitution']/10.)) + \
                     int(round(10*self['strength']/10.)) + \
                     int(round(10*(self.level+3)/4.)) + \
                     int(round(5*self['dexterity']/10.))

        # max stamina is only based on base stats (no mods)
        self.maxStamina = int(round(50*self.constitution/10.)) + \
                          int(round(20*self.strength/10.)) + \
                          int(round(20*(self.level+3)/4.)) + \
                          int(round(10*self.dexterity/10.))

        self.capacity = int(round(50 * self['strength']/10.))

        self.pv = 0
        self.ev = 10 + ((self['dexterity'] - 15)) + (self['speed'] - 10)/2
        self.hit = 10 + ((self['dexterity'] - 15)) + (self['speed'] - 10)/2

        self.damageSkew = 10 + (2*(self['strength'] - 10)) # just the skew

        # the assumption here is of weak, human-like unarmed dmg. monsters
        # should use maxUnarmedDamageMod to increase this for natural weapons,
        # etc.
        self.maxUnarmedDamage = max(0, 20 + 2*(self['strength'] - 10))

        # staminaRegen - this will really need to be tweaked. this is our rate
        # of regen per minute at the start of the day. over time staminaRegeMod
        # gets lowered. the rate at which that happens also needs to be tweaked
        self.staminaRegen = 20 + (self['constitution'] - 10)

        # actual visible intensity is this divided by 1000
        self.minVisibleLight = 40. - 1.8*(self['perception']-10)

    def __getitem__(self, stat):
        try:
            mod = getattr(self, stat+'Mod')
            return max(0, getattr(self, stat) + mod)
        except AttributeError:
            return getattr(self, stat)

    def timeOfAction(self, energy, speedNudge=0):
        #parole.debug('*** Time of action: %r energy, %r nudge', energy,
        #        speedNudge)
        rate = (float(self['speed'] + speedNudge) / 10.0) #* 1000.0
        time = float(energy) / rate
        return int(round(time))

    def inventoryWeight(self):
        return sum([item.weight for item in self.inventory])

    def inventoryCapacity(self):
        return self['capacity']

    def getFreeHands(self, n):
        parole.debug('free hands: %d', n)
        if self.__handsFree < n:
            return False
        assert(self.__handsFree > -1)
        self.__handsFree -= n
        return True

    def releaseHands(self, n):
        self.__handsFree = min(self.__handsFree + n, self.numHands)

    def visibleObjectsAt(self, tile):
        if not self.tileIsBrightEnough(tile):
            return []
        return [x for x in tile]

    def tileIsBrightEnough(self, tile):
        return magnitude(clampPositive(tile.availLight)) >= self['minVisibleLight']

    def throwEnergy(self):
        # TODO: speed should affect throwing energy, too
        avgE = 125
        diffFromAvg = self['strength'] - 10
        if diffFromAvg >= 0:
            return avgE + 10*diffFromAvg**2
        else:
            return max(0, avgE - 10*diffFromAvg**2)

    def statDeath(self):
        if self['strength'] < 0 or self['constitution'] < 0 or \
                self['dexterity']< 0 or self['speed'] < 0:
            return True
        return False

    def doFatigue(self, energy, markTime=True):
        self.energyLastAction = energy

        self.energyToStaminaLoss -= energy
        if self.energyToStaminaLoss <= 0:
            self.energyToStaminaLoss = self.energyPerStaminaLoss + \
                                       self.energyToStaminaLoss
            main.schedule.sendNow(Event('decrease stat', main.schedule.time, 
                    (self, 'stamina', 1), dests=[self]))

        # the statbox listens for this
        if markTime:
            self.timeLastAction = self.timeOfAction(energy)
            main.schedule.sendNow(Event('fatigue', main.schedule.time, (self,)))

    def scheduleStaminaRegen(self):
        # stamina regen rate is in stamina points per minute (=60,000 ms)
        # so we solve for the time of a 1-point increase in stamina
        if self['staminaRegen'] <= 0:
            return
        regenTime = main.schedule.time + int(round(60000./self['staminaRegen']))
        main.schedule.add(Event('regen stamina', regenTime, (self,),
            dests=[self]))

    def maintainStats(self):
        # check whether hp went below 0. this is the only place that
        # checks this! so something will only die if its hp drops below
        # 0 as the result of a 'set stat' event.
        if self['hp'] <= 0 or self.statDeath():
            self.die()
        else:
            # reset the bases of our derived stats to react to any changes
            self.setDerivedStatBases()

        # clamp hp to maxHP
        if self['maxHP'] < self['hp']:
            main.schedule.add(Event('set stat', main.schedule.time,
                    (self, 'hp', self['maxHP']), dests=[self])) 
        # clamp stamina to maxStamina
        if self['maxStamina'] < self['stamina']:
            main.schedule.add(Event('set stat', main.schedule.time,
                    (self, 'stamina', self['maxStamina']), dests=[self])) 

        # check for stamina effects
        if not self.getsTired:
            return
        staminaPercent = float(self['stamina']) / float(self['maxStamina'])
        if staminaPercent <= self.exhaustedPercent:
            newStaminaLevel = 'exhausted'
        elif staminaPercent <= self.fatiguedPercent:
            newStaminaLevel = 'fatigued'
        elif staminaPercent <= self.windedPercent:
            newStaminaLevel = 'winded'
        else:
            newStaminaLevel = None
        if newStaminaLevel != self.staminaLevel:
            self.setStaminaLevel(newStaminaLevel)

    def staminaLevelColorized(self):
        if self.staminaLevel == 'exhausted':
            return '{\\Violet exhausted}'
        elif self.staminaLevel == 'fatigued':
            return '{\\Orange fatigued}'
        elif self.staminaLevel == 'winded':
            return '{\\Yellow winded}'
        return None


    def setStaminaLevel(self, level):
        parole.debug('%r: setting new stamina level %r (old %r)', self, level,
                self.staminaLevel)

        if self.staminaLevel and main.statusBox and self is main.statusBox.creature:
            main.statusBox.statusNotes.remove(self.staminaLevelColorized())

        # first remove effects of current stamina level
        if self.staminaLevel == 'exhausted':
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self, 'strengthMod', 9), dests=[self], options={'noMsg':1}))
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self, 'dexterityMod', 9), dests=[self], options={'noMsg':1}))
        elif self.staminaLevel == 'fatigued':
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self, 'strengthMod', 3), dests=[self], options={'noMsg':1}))
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self, 'dexterityMod', 3), dests=[self], options={'noMsg':1}))
        elif self.staminaLevel == 'winded':
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self, 'strengthMod', 1), dests=[self], options={'noMsg':1}))
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self, 'dexterityMod', 1), dests=[self], options={'noMsg':1}))

        self.staminaLevel = level

        # then set effects of new stamina level
        if level == 'exhausted':
            main.schedule.add(Event('decrease stat', main.schedule.time,
                (self, 'strengthMod', 9), dests=[self], options={'noMsg':1}))
            main.schedule.add(Event('decrease stat', main.schedule.time,
                (self, 'dexterityMod', 9), dests=[self], options={'noMsg':1}))
        elif level == 'fatigued':
            main.schedule.add(Event('decrease stat', main.schedule.time,
                (self, 'strengthMod', 3), dests=[self], options={'noMsg':1}))
            main.schedule.add(Event('decrease stat', main.schedule.time,
                (self, 'dexterityMod', 3), dests=[self], options={'noMsg':1}))
        elif level == 'winded':
            main.schedule.add(Event('decrease stat', main.schedule.time,
                (self, 'strengthMod', 1), dests=[self], options={'noMsg':1}))
            main.schedule.add(Event('decrease stat', main.schedule.time,
                (self, 'dexterityMod', 1), dests=[self], options={'noMsg':1}))

        if level:
            #main.transient('You are %s!' % level)
            if main.statusBox and self is main.statusBox.creature:
                main.statusBox.statusNotes.append(self.staminaLevelColorized())
        #else:
        #    main.transient('You feel energetic once more.')

    def blockedBy(self, obj):
        import dungeon
        if obj is self:
            return False

        if not self.corporeal:
            return obj.blocksNonCorporeal

        return obj.blocksMove

    def blockedAt(self, tile):
        for obj in tile:
            if self.blockedBy(obj):
                return True
        return False

    def listen(self, event):
        """
        Handles:
            "enter inventory"  (item, creature)
            "leave inventory"  (item, creature)
            "wear"             (item, creature, slot)
            "remove"           (item, creature, slot)
            "set stat"         (creature, statname, value)
            "increase stat"    (creature, statname, amount)
            "decrease stat"    (creature, statname, amount)
            "receive melee"   (target, source, weapon)
            "receive damage"   (target, source, weapon, amount)
            "enter tile"       (obj, pos, map)
            "regen stamina"    (creature)
            "quaff"             (creature, potion)
            "read"             (creature, scroll)
            "statusNote+"      (creature, statusNote)
            "statusNote-"      (creature, statusNote)
        """

        if event in self.pendingEvents:
            self.pendingEvents.remove(event)

        if self.dead:
            # we handle nothing if we are dead, not even events for the
            # superclass
            # subclasses of Creature should do the same
            return

        sim.Obj.listen(self, event)
        map = None
        if self.parentTile:
            map = self.parentTile.map

        if event.id == "enter inventory":
            item, creature = event.args
            if creature is self:
                noMsg = ('noMsg' in event.options and event.options['noMsg'])
                #parole.debug('%r: handling "enter inventory"...', self)
                #main.schedule.add(Event('leave tile', main.schedule.time,
                #        (item, self.pos), dests=[item]))
                if item not in map[self.pos]:
                    # something must have moved since scheduling the event
                    if self is main.player and not noMsg:
                        main.transient("%s is no longer in reach!" %
                                item.noun(definite=1, capitalized=1), item.pos)
                    return

                map[self.pos].remove(item)
                self.inventory.append(item)
                assert(not item.carrier) # must leave another creature's inv
                                         # before entering this one's
                item.carrier = self
                if creature is main.player and not noMsg:
                    n = item.noun(definite=1)
                    main.transient("You pick up %s." % item.noun(definite=1))
                    #main.transientAnnotes.append(main.mapFrame.annotate(
                    #    creature.parentTile,
                    #    "You pick up %s." % item.noun(definite=1)))
                elif visible(creature) and not noMsg:
                    main.transient("%s picks up %s." % (creature.noun(definite=1,
                            capitalized=1), item.noun(definite=1)),
                            creature.parentTile)
                    #main.transientAnnotes.append(main.mapFrame.annotate(
                    #    creature.parentTile,
                    #    "%s picks up %s." % (creature.noun(definite=1,
                    #        capitalized=1), item.noun(definite=1))))
            #parole.debug('%r: handled "enter inventory".', self)

        elif event.id == "leave inventory":
            assert(map)
            item, creature = event.args
            if creature is self:
                if isinstance(item, sim_items.Wearable) and item.wearer:
                    if creature is main.player:
                        main.transientAnnotes.append(main.mapFrame.annotate(
                            creature.parentTile,
                            "You must remove %s first." % item.noun(definite=1)))
                    return

                if isinstance(item, sim_items.LightSource) and item.on:
                    self.releaseHands(item.handsNeeded)

                self.inventory.remove(item)
                item.carrier = None
                map[self.pos].add(item)
                main.schedule(Event('enter tile', main.schedule.time,
                        (item, self.pos, map), dests=[item]))
                noMsg = ('noMsg' in event.options and event.options['noMsg'])
                if creature is main.player and not noMsg:
                    main.transientAnnotes.append(main.mapFrame.annotate(
                        creature.parentTile,
                        "You drop %s." % item.noun(definite=1)))
                elif visible(creature) and not noMsg:
                    main.transientAnnotes.append(main.mapFrame.annotate(
                        creature.parentTile,
                        "%s drops %s." % (creature.noun(definite=1,
                            capitalized=1), item.noun(definite=0))))

        elif event.id == 'wear':
            item, creature, slot = event.args
            if creature is self:
                if ('noMsg' not in event.options) or (not event.options['noMsg']):
                    # "wield" verb for weapons
                    if slot == 'weapon':
                        verb = 'wield'
                    elif slot == 'shield':
                        verb = 'ready'
                    else:
                        verb = 'don'
                    if creature is main.player:
                        main.transient("You %s %s." % (verb,
                            item.noun(definite=True)))
                    elif visible(creature):
                        main.transient("%s %ss %s." % (creature.noun(definite=True,
                            capitalized=True), verb, item.noun()), creature.parentTile)

                self.wearSlots[slot] = item
                item.wearer = self
                item.wearerSlot = slot

        elif event.id == 'remove':
            item, creature, slot = event.args
            if creature is self:
                self.wearSlots[slot] = None
                item.wearer = None
                item.wearerSlot = None
                self.releaseHands(item.handsNeeded)

                if ('noMsg' not in event.options) or (not event.options['noMsg']):
                    # "unwield" verb for weapons
                    if slot == 'weapon':
                        verb = 'unwield'
                    else:
                        verb = 'remove'
                    if creature is main.player:
                        main.transient("You %s %s." % (verb,
                            item.noun(definite=True)))
                    elif visible(creature):
                        main.transient("%s %ss %s." % (creature.noun(definite=True,
                            capitalized=True), verb, item.noun()), creature.parentTile)

        #elif event.id == 'set stat':
        #    creature, statname, value = event.args
        #    if creature is self:
        #        if statname not in self.statNames:
        #            raise ValueError('Got "set stat" event with bad statname '
        #                    '%r' % statname)
        #        newVal = int(round(value))
        #        if 'Mod' not in statname and statname not in ('hp', 'stamina'):
        #            newVal = max(0, newVal)
        #        setattr(self, statname, newVal) 
        #        self.maintainStats()

        elif event.id in ('set stat', 'increase stat', 'decrease stat'):
            creature, statname, amount = event.args
            if creature is self:
                if 'noMsg' in event.options and event.options['noMsg']:
                    noMsg = True
                else:
                    noMsg = False
                statusNoteAdd = None
                if 'statusNote+' in event.options:
                    statusNoteAdd = event.options['statusNote+']
                statusNoteRem = None
                if 'statusNote-' in event.options:
                    statusNoteRem = event.options['statusNote-']

                if statname not in self.statNames:
                    raise ValueError('Got %r event with bad statname '
                            '%r' % (event.id, statname))
                curStat = getattr(self, statname)

                if event.id == 'set stat':
                    newVal = int(round(amount))
                else:
                    change = (event.id[0]=='i' and 1 or -1)*amount
                    newVal = int(round(curStat+change))
                if 'Mod' not in statname and statname != 'hp':
                    newVal = max(0, newVal)
                setattr(self, statname, newVal)
                self.maintainStats()

                # continue the stamina regen depreciation cycle
                if statname == 'staminaRegenMod' and \
                        event.id == 'decrease stat':
                    main.schedule.add(Event('decrease stat', 
                        main.schedule.time + self.regenLossTime, 
                        (self, 'staminaRegenMod', 1), dests=[self]))

                if 'Mod' in statname:
                    baseStat = statname[:-3]
                else:
                    baseStat = statname
                if baseStat in self.statChanges and (self is main.player or \
                        visible(self)) and newVal != curStat and not noMsg:
                    desc = self.statChanges[baseStat][int(newVal < curStat)]
                    if self is main.player:
                        main.transient('You feel %s.' % desc)
                    else:
                        main.transient('%s looks %s.' % (self.noun(definite=1,
                            capitalized=1), desc))

                if main.statusBox and self is main.statusBox.creature and \
                        (statusNoteAdd or statusNoteRem):
                    if statusNoteAdd:
                        main.statusBox.statusNotes.append(statusNoteAdd)
                    if statusNoteRem and statusNoteRem in main.statusBox.statusNotes:
                        main.statusBox.statusNotes.remove(statusNoteRem)
                    main.statusBox.touch()

        elif event.id == 'receive melee':
            target, source, weapon = event.args
            if target is self:
                parole.debug('%r received attack from %r wielding %r' % event.args)
                self.doFatigue(self.receiveAttackEnergy, markTime=False)
            elif source is self:
                self.meleeTarget = None

            if target is self and (not self.corporeal) and source.pos in\
                    neighbors(self.pos):
                # attacker and self.creature switch places (instantaneously...
                # can this be exploited?)
                selfPos = self.pos
                attackerPos = source.pos
                srctile = source.parentTile
                srctile.remove(source)
                if self.blockedAt(srctile):
                    srctile.add(source)
                    return
                selftile = self.parentTile
                selftile.remove(self)
                if source.blockedAt(selftile):
                    srctile.add(source)
                    selftile.add(self)
                    return
                selftile.add(self)

                if source is main.player:
                    if visible(self):
                        main.transient("You pass through %s!" %
                            self.noun(definite=1), selfPos)
                    else:
                        main.transient("You pass through something misty!", selfPos)
                elif self is main.player:
                    if visible(srctile):
                        main.transient("%s passes through you!" %
                            source.noun(definite=1, capitalized=1))
                    else:
                        main.transient("Something passes through you!")
                elif visible(self) and visible(srctile):
                    main.transient("%s passes through %s!" %
                            (source.noun(definite=1, capitalized=1),
                                self.noun(definite=1)), selfPos)
                main.schedule.sendNow(Event('translocate', main.schedule.time, 
                    args=(self, attackerPos, map), dests=[self]))
                main.schedule.sendNow(Event('translocate', main.schedule.time, 
                    args=(source, selfPos, map), dests=[source]))

        elif event.id == 'receive damage':
            # TODO: in the future, only certain kinds of damage (i.e. physical)
            # should be reduced by pv.
            target, source, weapon, amount = event.args
            if target is self:
                if source and source.dead:
                    #wtf
                    parole.error('Got "receive damage" event from dead source!')
                    return

                if 'confirm' in event.options and event.options['confirm']:
                    confirmFunc = event.options['confirm']
                    if not confirmFunc(self):
                        return

                if 'type' in event.options and event.options['type']=='melee'\
                        and source.pos not in neighbors(self.pos):
                    # we have gotten away from our attacker before he could
                    # finish his attack
                    if source is main.player:
                        main.transient('%s withdraws before you can land your '
                                'blow!' % self.noun(definite=1, capitalized=1),
                                source.pos)
                    else:
                        if visible(source):
                            if self is main.player:
                                main.transient('You withdraw before %s can '
                                        'land %s blow!' %\
                                        (source.noun(definite=1),
                                            source.pronounGen()), source.pos)
                            else:
                                main.transient('%s withdraws before %s can '
                                        'land %s blow!' % (self.noun(definite=1,
                                            capitalized=1),
                                            source.noun(definite=1),
                                            source.pronounGen()), source.pos)
                    return


                if 'transient' in event.options:
                    main.transient(event.options['transient'], self.parentTile)
                protection = self.protectionDraw()
                amount -= protection
                if amount <= 0:
                    # TODO: absorbed damage message
                    if self is main.player:
                        main.transient('Your armor absorbs all the damage!')
                    elif visible(self):
                        main.transient('%s armor absorbs all the damage!' % \
                                self.pronounGen(capitalized=1), self.parentTile)
                    return

                # getting hurt costs fatigue
                self.doFatigue(int(self.damageEnergyPerHP*amount),
                        markTime=False)

                # blood splatter. TODO: use source weight if no weapon
                if self.bloodClass and source and source.pos and \
                        self.parentTile and self.pos:
                    splatter = sim.Splatter(self.bloodClass, min(amount, self['hp']))
                    diff = difference(self.pos, source.pos)
                    if diff[0] == 0 and diff[1] == 0:
                        slope = (0,0)
                    else:
                        slope = rounded(normalized(diff))
                    splatter.apply(self.parentTile, slope, 
                            weapon and weapon.weight or (source['strength']/3))
                    #self.parentTile.map.update()

                self.lastDamageSource = source
                parole.debug('%r received %s damage.', self, amount)
                main.schedule.sendNow(Event('decrease stat', main.schedule.time,
                        (self, 'hp', amount), dests=[self]))

        elif event.id == 'enter tile':
            obj, pos, map = event.args
            if obj is self:
                # if we're entering a tile that is too dim to see in, apply
                # blindness penalties
                if not self.tileIsBrightEnough(self.parentTile):
                    self.goBlind()

                # if we're entering a tile that is bright enough to see in, 
                # remove blindness penalties
                else:
                    self.goUnblind()

        elif event.id == 'regen stamina':
            creature = event.args[0]
            if creature is self:
                if (not self.meleeTarget) and self['stamina'] < self['maxStamina']:
                    main.schedule.sendNow(Event('increase stat', main.schedule.time,
                            (self, 'stamina', 10), dests=[self]))
                self.scheduleStaminaRegen()

        elif event.id == 'quaff':
            creature, potion = event.args
            if creature is self:
                potion.quaff(self)

        elif event.id == 'read':
            creature, scroll = event.args
            if creature is self:
                scroll.read(self)

        elif event.id == 'statusNote+':
            if main.statusBox and self is main.statusBox.creature and \
                    event.args[0] is self:
                main.statusBox.statusNotes.append(event.args[1])
                main.statusBox.touch()

        elif event.id == 'statusNote-':
            if main.statusBox and self is main.statusBox.creature and \
                    event.args[0] is self:
                main.statusBox.statusNotes.remove(event.args[1])
                main.statusBox.touch()



    # actions. return scheduled time of completion or -1 if action failed and
    # was not scheduled
    def toggleDoor(self, door):
        # Make sure the door is adjacent
        if door.parentTile not in adjacentTiles(self.parentTile):
            return -1


        time = main.schedule.time + self.timeOfAction(self.toggleDoorEnergy)
        self.doFatigue(self.toggleDoorEnergy)
        main.schedule.add(Event('door', 
            time, (door, 'toggle'), dests=[door]))
        parole.debug('%s: toggling door at %d', self.noun(), time)
        return time

    def pickupItem(self, item):
        # make sure the item is in our tile
        if item.parentTile is not self.parentTile:
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is too far away to pick up." % item.noun(definite=True,
                    capitalized=True)))
            return -1

        # make sure the item is not too heavy
        if item.weight + self.inventoryWeight() > self.inventoryCapacity():
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is too heavy to carry with your other items." \
                 % item.noun(definite=True, capitalized=True)))
            return -1

        if isinstance(item, sim_items.LightSource) and item.on:
            if not self.getFreeHands(item.handsNeeded):
                main.transient("You don't have enough free hands to hold that "\
                        "while it's lit!")
                return -1

        energy = self.pickupBaseEnergy + self.pickupWeightCoefEnergy*item.weight
        time = main.schedule.time + self.timeOfAction(energy)
        self.doFatigue(energy)
        #main.schedule.add(Event('enter inventory', time, (item, self),
        #    dests=[self, item]))

        # enter inventory instantly, then wait for a time
        main.schedule.add(Event('enter inventory', main.schedule.time, (item, self),
            dests=[self, item]))
        #parole.debug('will pickup at %d', time)
        return time

    def dropItem(self, item):
        # make sure the item is in our inventory
        if item.carrier is not self:
            # how would this ever happen?
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is not in your inventory!" % item.noun(definite=True,
                    capitalized=True)))
            return -1
        assert(item in self.inventory)

        time = main.schedule.time + self.timeOfAction(self.dropEnergy)
        self.doFatigue(self.dropEnergy)
        main.schedule.add(Event('leave inventory', time, (item, self),
            dests=[self, item]))
        return time

    def wear(self, item, slot=None, noMsg=False, instantly=False):
        # make sure the item is in our inventory
        if item.carrier is not self:
            # how would this ever happen?
            main.transient( "%s is not in your inventory!" %
                    item.noun(definite=True, capitalized=True))
            return -1
        assert(item in self.inventory)
        if not isinstance(item, sim_items.Wearable):
            # how would this ever happen?
            main.transient(
                "%s is not wearable or wieldable!" % item.noun(definite=True,
                    capitalized=True))
            return -1

        time = main.schedule.time

        slot = slot or item.wearSlots[0]
        if self.wearSlots[slot] and (not instantly):
            if self.wearSlots[slot].handsNeeded < item.handsNeeded:
                main.transient('You do not have enough free hands for %s!' %\
                        item.noun(definite=1))
                return -1
            # we have to remove a currently worn thing before wearing this one
            time = self.remove(self.wearSlots[slot])
            assert(time > -1)

            ## player has to remove something first
            #if slot == 'weapon':
            #    verb = 'unwield'
            #else:
            #    verb = 'remove'
            #main.transient('You must first %s %s.' %
            #        (verb, self.wearSlots[slot].noun(),), self.parentTile)
            #return -1

        # get free hands
        elif not self.getFreeHands(item.handsNeeded) and not noMsg:
            main.transient('You do not have enough free hands for %s!' %\
                    item.noun(definite=1))
            return -1

        if not instantly:
            time += self.timeOfAction(item.wearEnergy)
        self.doFatigue(item.wearEnergy)
        main.schedule.add(Event('wear', time, (item, self, slot),
            dests=[self, item], options={'noMsg':noMsg}))
        return time

    def isWearing(self, item):
        """
        If this Creature is currently wearing the given item, returns the slot
        it is worn in, otherwise returns None.
        """
        for slot in self.wearSlots:
            if self.wearSlots[slot] is item:
                return slot
        return None


    def remove(self, item):
        slot = self.isWearing(item)
        if not slot:
            main.transient('You are not wearing or wielding %s!' %
                    (item.noun(definite=1),), self.parentTile)
            return -1

        time = main.schedule.time + self.timeOfAction(item.wearEnergy)
        self.doFatigue(item.wearEnergy)
        main.schedule.add(Event('remove', time, (item, self, slot),
            dests=[self, item]))
        return time

    def melee(self, targetCreature):
        if targetCreature.parentTile not in adjacentTiles(self.parentTile):
            # how did this happen?
            parole.error('Creature "%r" tried melee nonadjacent target "%r"',
                    self, targetCreature)
            parole.error('target.parentTile: %r, self.parentTile: %r',
                    targetCreature.parentTile, self.parentTile)
            assert(False)

        # get info from weapon
        if ('weapon' in self.wearSlots) and (self.wearSlots['weapon']):
            weapon = self.wearSlots['weapon']
            verb = weapon.verb
            attackEnergy = weapon.attackEnergy
        else:
            weapon = None
            verb = self.unarmedVerb
            attackEnergy = self.unarmedAttackEnergy

        # when will we be done attacking?
        attackTime = main.schedule.time + self.timeOfAction(attackEnergy)
        self.doFatigue(attackEnergy)

        if weapon:
            self.doFatigue(weapon.wieldEnergy, markTime=False)

        # mark ourselves as in melee
        self.meleeTarget = targetCreature

        # schedule a "receive melee" event, regardless of whether we hit
        main.schedule.add(Event("receive melee", attackTime, (targetCreature,
            self, weapon), dests=[self, targetCreature]))

        # see whether our attack draw beats the target's defense draw
        if self.attackDraw(targetCreature) > targetCreature.defenseDraw(self):
            # it's a hit
            if main.player is self:
                if targetCreature.brain.fleeing and visible(targetCreature):
                    transient='You %s %s from behind as %s tries to flee!'\
                        % (verb,
                        visible(targetCreature) and targetCreature.noun(definite=1)\
                            or 'something', targetCreature.pronounNom())
                else:
                    transient='You %s %s.' % (verb,
                    visible(targetCreature) and targetCreature.noun(definite=1)\
                            or 'something')
            elif main.player is targetCreature:
                transient='%s %s you.' % \
                        (visible(self) and self.noun(definite=1,
                    capitalized=1) or 'Something', thirdperson(verb))

            # draw damage
            if weapon:
                damage = weapon.drawDamage(self, targetCreature)
            else:
                damage = self.drawUnarmedDamage(targetCreature)

            # schedule a "receive damage" event
            rcvDmg = Event("receive damage", attackTime, (targetCreature, self,
                weapon, damage), dests=[targetCreature, self],
                options={'transient':transient, 'type':'melee'})
            main.schedule.add(rcvDmg)
            self.pendingEvents.append(rcvDmg)
        else:
            # it's a miss
            if main.player is self:
                main.transient('You miss %s.' % \
                        (visible(targetCreature) and \
                        targetCreature.noun(definite=1) or 'something',),
                        targetCreature.parentTile)
            elif main.player is targetCreature:
                main.transient('%s misses you.' % \
                        (visible(self) and self.noun(definite=1, capitalized=1) \
                        or 'Something',),
                        targetCreature.parentTile)

        return attackTime

    def throw(self, item, destination):
        # make sure the item is in our inventory
        if item.carrier is not self:
            # how would this ever happen?
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is not in your inventory!" % item.noun(definite=True,
                    capitalized=True)))
            return -1
        assert(item in self.inventory)

        slot = self.isWearing(item)
        if slot:
            main.schedule.sendNow(Event('remove', main.schedule.time, (item,
                self, slot), dests=[self, item]))

        # energy units are something like pounds*tiles^2/s^2 (a "tile
        # pound-force", if you will).
        E = float(self.throwEnergy())
        m = float(item.weight)
        v0 = sim.Trajectory.velocityAtEnergy(E, m)
        parole.debug('Throw: E = %f, v0 = %f.', E, v0)

        # throwing causes fatigue
        fatigueE = 10 * E
        self.doFatigue(fatigueE)

        # how long does it take to throw this thing?
        time = main.schedule.time + self.timeOfAction(fatigueE)
        parole.debug('Throw: time to throw = %dms.',
                self.timeOfAction(fatigueE))

        # leave inventory now
        main.schedule.sendNow(Event('leave inventory', main.schedule.time,
            (item, self), dests=[self, item], options={'noMsg':1}))

        # message
        if self is main.player:
            main.transient('You throw %s.' % item.noun(definite=1))
        else:
            main.transient('%s throws %s.' % (self.noun(definite=1,
                capitalized=1), item.noun(definite=1)))

        # start the "physics" simulation, which advances the item
        # tile-by-tile according to initial velocity, gravity, and friction.
        trajectory = sim.Trajectory(item, liableParty=self)
        trajectory.start(self.parentTile, destination, time, v0)

        return time

    def die(self, dieVerb=None, corpsePossible=True):
        """
        Causes the creature to die immediately, regardless of hp,
        removes the creature from its tile, creates a corpse, adds the corpse
        to the tile, causes the creature to stop handling any events.
        Any inventory is also immediately added to the tile
        """
        self.dead = True
        if self is main.player:
            main.transient('You %s.' % (dieVerb or self.dieVerb,))
        else:
            main.transient('%s %s.' % \
                    (visible(self) and self.noun(definite=1, capitalized=1) \
                     or 'Something',
                    thirdperson(dieVerb or self.dieVerb)),
                    self.parentTile)

        # drop inventory immediately
        for item in self.inventory:
            parole.debug('Death-dropping %s', item)
            main.schedule.sendNow(Event('leave inventory', main.schedule.time,
                (item, self), dests=[self, item], options={'noMsg':1}))

        if corpsePossible:
            corpse = self.makeCorpse()
            if corpse:
                self.parentTile.add(corpse)
        self.parentTile.remove(self)

        main.schedule.removeListener(self) # stop receiving any events
        for event in self.pendingEvents:
            if event in main.schedule:
                main.schedule.remove(event)

    def updateBlindness(self):
        pass

    def goBlind(self):
        if self.blind:
            return
        self.blind = True
        #main.schedule.add(Event('decrease stat', main.schedule.time, 
        #        (self, 'evMod', 1), dests=[self]))
        #main.schedule.add(Event('decrease stat', main.schedule.time, 
        #        (self, 'hitMod', 1), dests=[self]))
        main.schedule.sendNow(Event('decrease stat', main.schedule.time, 
            (self, 'speedMod', 2), dests=[self], options={'noMsg':1}))
        #if self is main.player:
        #    main.transient("You can't see where you are.")

    def goUnblind(self):
        if not self.blind:
            return
        self.blind = False
        #main.schedule.add(Event('increase stat', main.schedule.time, 
        #        (self, 'evMod', 1), dests=[self]))
        #main.schedule.add(Event('increase stat', main.schedule.time, 
        #        (self, 'hitMod', 1), dests=[self]))
        main.schedule.sendNow(Event('increase stat', main.schedule.time, 
            (self, 'speedMod', 2), dests=[self], options={'noMsg':1}))
        #if self is main.player:
        #    main.transient("You can see yourself again.")

    def makeCorpse(self):
        if not self.leavesCorpse:
            return None

        corpse = sim.Obj('{\DarkGray %s corpse}' % (self.name,), 5,
                parole.map.AsciiTile('%', self.shader.rgb),
                startsVowel=self.startsVowel)
        return corpse

    def attackDraw(self, targetCreature):
        # unseen attacker bonus
        unseenBonus = 0
        if targetCreature is main.player and not visible(self):
            unseenBonus = 10
        return random.betavariate(max(1,self['hit']+unseenBonus), 10)

    def defenseDraw(self, attacker):
        # TODO: blindness effects
        return random.betavariate(max(1, self['ev']), 10)

    def protectionDraw(self):
        # so, mean protection will be pv
        return random.betavariate(10, 10) * 2 * self['pv']

    def drawUnarmedDamage(self, target):
        # at some point might want to be sensitive to who the target is
        return random.betavariate(max(1, self['damageSkew']), 10) \
                * self['maxUnarmedDamage']

#=========================================

class Monster(Creature):
    """
    A Monster is a Creature that is always assumed hostile to the player, and
    which has some sort of fighting AI
    """

    defaultLightTolerance = 3.0

    def __init__(self, name, shader, strength, dexterity, constitution,
            perception, speed, level, xpValue, startsVowel=False,
            wearslots=None, unarmedVerb=None, dieVerb='die', leavesCorpse=True,
            unarmedAttackEnergy=None, description=None, brain=None,
            bloodClass=RedBlood, lightSensitivity=0, lightTolerance=-1,
            corporeal=True, feelsPain=True, getsTired=True):

        Creature.__init__(self, name, shader, strength, dexterity, constitution,
                perception, speed, level, startsVowel, wearslots, unarmedVerb,
                dieVerb, leavesCorpse, unarmedAttackEnergy, description,
                bloodClass=bloodClass, corporeal=corporeal, feelsPain=feelsPain,
                getsTired=getsTired)
        self.xpValue = xpValue
        self.brain = brain or ai.DefaultMonsterBrain(self, ai.defaultStateMap)
        self.lightSensitivity = lightSensitivity 
        self.lightTolerance = lightTolerance < 0 and\
                self.defaultLightTolerance or lightTolerance

    def die(self, dieVerb=None, corpsePossible=True):
        Creature.die(self, dieVerb, corpsePossible)

        # give xp to our killer
        if isinstance(self.lastDamageSource, Creature):
            main.schedule.sendNow(Event('increase stat', main.schedule.time,
                (self.lastDamageSource, 'xp', self.xpValue),
                dests=[self.lastDamageSource]))
        else:
            parole.info('*** %r: Died without a killer!', self)

        # remove our brain from any event listening
        main.schedule.removeListener(self.brain)

    def listen(self, event):
        """
        Passes events on to Creature and to this creature's brain.
        """
        if not self.dead:
            Creature.listen(self, event)
            self.brain.listen(event)

#=========================================

class NPC(Creature):
    """
    An NPC is a Creature that is friendly to the player and can talk. Can't
    attack or be attacked.
    """
    # constructor same as Creature

    def say(self, words):
        if visible(self):
            main.transient(words, self.parentTile)


#=========================================

class Player(Creature):
    """
    The Creature that the player actually controls.
    """

    # The stuff that the player starts with
    startingKit = [
        'dagger',    # these refer to script names
        'shirt',
        'sandals',
        'torch',
        'healing',
    ]

    # XP required to reach each level. level : xp.
    levelXP = {
        2:  180,
        3:  360,
        4:  720,
        5:  1440,
        6:  2880,
        7:  5760,
    }

    maxSparkWeight = 5 # can't light anything over this weight without using a
                   # torch/lantern
    snuffEnergy = 2000
    quaffEnergy = 2000
    readEnergy = 1000
    climbEnergy = 30000 # climbing up/down mineshafts
    jumpEnergy = 4000 # jumping down mineshafts

    unidMeanings = ['light', 'see', 'go', 'near', 'far', 'more', 'less', 'have',
            'life']
    
    def __init__(self, playerName='Unknown Player', str=10, dex=10, con=10,
            per=10, spd=10):
        Creature.__init__(self, playerName, parole.map.AsciiTile('@',
            (255,255,255)), str, dex, con, per, spd, 1)
        self.hasProperName = True
        self.layer = 12000
        self.playerName = playerName

        #self.acquireStartingKit()

        # wizard/debug mode stuff
        self.monsterReporting = False
        self.clipping = True

        self.winner = False
        self.encounteredMonsters = set()
        self.levelsSeen = set()

        # TODO: this should be subsumed by a general effects tracker
        self.farsight = False

        # create the language that will be used to generate unidentified
        # comestibles' labels
        self.language = language.Language(Player.unidMeanings)

        self.identifiedItemClasses = set()
        self.highlightedTiles = {} # Tile -> ReticleOverlayShader

        main.schedule.listen('start sim', self)

    def acquireStartingKit(self):
        slotsUsed = []
        for script in self.startingKit:
            for type in ('weapons', 'armor', 'scrolls', 'potions', 'lights'):
                itemClass = parole.resource.getObject(
                    'scripts/%s/%s.py' % (type, script), 
                    'thingClass')
                if itemClass:
                    break

            if not itemClass:
                parole.error('Unable to find requested item class in '\
                        'starting kit: %r', script)
                assert(False)

            item = itemClass()
            self.inventory.append(item)
            item.carrier = self
            #main.schedule.sendNow(Event('enter inventory', main.schedule.time,
            #    (item, self), dests=[item], options={'noMsg':1}))

            if isinstance(item, sim_items.Wearable):
                slot = item.wearSlots[0]
                if slot not in slotsUsed:
                    slotsUsed.append(slot)
                    self.wear(item, noMsg=True, instantly=True)

    def identify(self, obj, message=True, messageLoc=None, msgTemplate=None):
        if not isinstance(obj, sim_items.Item):
            raise TypeError('Only Items can be identified!')
        if obj.__class__ not in self.identifiedItemClasses:
            unidNoun = obj.noun(definite=1, capitalized=0)
            self.identifiedItemClasses.add(obj.__class__)
            idNoun = obj.noun()
            if message:
                msgTemplate = msgTemplate or random.choice([
                    '%s proves to be %s.',
                    '%s seems to be %s.',
                    '%s turns out to be %s.',
                    'It seems that %s is in fact %s.',
                    '%s must actually be %s.',
                    'You infer that %s is actually %s.'
                ])
                msg = msgTemplate % (unidNoun, idNoun)
                msg = msg[0].upper() + msg[1:]
                main.transient(msg, messageLoc)

    def recognizes(self, obj):
        return obj.__class__ in self.identifiedItemClasses

    def light(self, target):
        if not isinstance(target, sim_items.LightSource) and not isinstance(target,
                sim_items.Scroll):
            raise TypeError('target must be a LightSource or Scroll')
        if isinstance(target, sim_items.LightSource) and target.on:
            main.transient('%s is already lit!' % target.noun(definite=1,
                capitalized=1))
            return -1

        #haveTorch = self.wearSlots['light'] and self.wearSlots['light'].on
        source = None
        for obj in self.inventory:
            if isinstance(obj, sim_items.LightSource) and obj.on:
                source = obj
        if not source and target.weight > self.maxSparkWeight:
            main.transient('%s is too big to light with your flint. '
                           "You'll need a torch or something." % \
                                   target.noun(definite=1, capitalized=1))
            return -1


        energy = target.lightEnergy
        if source:
            energy /= 2

        time = main.schedule.time + self.timeOfAction(energy)

        if isinstance(target, sim_items.LightSource):
            if target in self.inventory and not self.getFreeHands(target.handsNeeded):
                main.transient("You don't have enough free hands to hold that "\
                               "while it's lit!")
                return -1
            main.schedule.add(Event('activate light', time, (target,),
                dests=[target]))
        elif isinstance(target, sim_items.Scroll):
            if target in self.inventory:
                target.carrier = None
                flamingScroll = sim_items.FlamingScroll(self.parentTile.map,
                        (self.parentTile.col, self.parentTile.row))
                flamingScroll.carrier = self
                self.inventory[self.inventory.index(target)] = flamingScroll

            else:
                t = target.parentTile
                t.remove(target)
                t.add(sim_items.FlamingScroll(t.map, (t.col, t.row)))

            main.transient('You light %s ablaze.' % target.noun(definite=1,
                capitalized=0))
             
        self.doFatigue(energy)
        return time

    def snuff(self, target):
        if not isinstance(target, sim_items.LightSource):
            raise TypeError('target must be a LightSource')
        if not target.on:
            main.transient('%s is already unlit!' % target.noun(definite=1,
                capitalized=1))
            return -1
        if not target.snuffable:
            main.transient('%s cannot be extinguished.' %
                    target.noun(definite=1, capitalized=1))
            return -1

        time = main.schedule.time + self.timeOfAction(self.snuffEnergy)
        self.doFatigue(self.snuffEnergy)
        #if target in self.inventory and isinstance(target, LightSource):
        #    self.releaseHands(target.handsNeeded)

        main.schedule.add(Event('deactivate light', time, (target,),
            dests=[target]))
        return time

    def quaff(self, target):
        time = main.schedule.time + self.timeOfAction(self.quaffEnergy)
        self.doFatigue(self.quaffEnergy)

        main.schedule.add(Event('quaff', time, (self, target,),
            dests=[self]))
        # quaffing anything replenishes some stamina
        main.schedule.add(Event('increase stat', time, (self, 'stamina', 40),
            dests=[self]))

        self.inventory.remove(target)
        return time

    def read(self, target):
        time = main.schedule.time + self.timeOfAction(self.readEnergy)
        self.doFatigue(self.readEnergy)

        main.schedule.add(Event('read', time, (self, target,),
            dests=[self]))

        self.inventory.remove(target)
        return time

    def bindMe(self):
        if self.farsight:
            return
        if main.mapFrame.fovObj is not self or \
                main.mapFrame.fovRad != self['perception']:
            remember = False
            #if main.mapFrame.getMap().name == 'town':
            #    remember = True
            main.mapFrame.bindVisibilityToFOV(self, self['perception'],
                    remember=remember, fovCondition=self.tileIsBrightEnough)
            main.mapFrame.getMap().update()

    def updateHighlights(self):
        # highlight visible items/monster/stairs, according to options
        for tile,overlay in self.highlightedTiles.iteritems():
            tile.removeOverlay(overlay)
        self.highlightedTiles.clear()
        upPos, downPos = main.levels[main.currentLevel][1:]
        for pos in main.mapFrame.visibleTiles:
            tile = main.mapFrame.getMap()[pos]
            if not visible(tile):
                continue
            if pos in (upPos, downPos) and main.currentLevel != 'town' and \
                    parole.conf.interface.highlight.stairs:
                overlay = parole.map.ReticleOverlayShader(tile.size,
                    tuple(parole.conf.interface.highlight.stairsRGB))
                tile.addOverlay(overlay)
                self.highlightedTiles[tile] = overlay
                # stairs take precedence over items, monsters
                continue

            hasItem, hasMonster, hasNPC = False, False, False
            for obj in tile:
                if isinstance(obj, sim_items.Item):
                    hasItem = True
                elif isinstance(obj, Monster):
                    hasMonster = True
                elif isinstance(obj, NPC):
                    hasNPC = True

            if hasMonster and parole.conf.interface.highlight.monsters:
                overlay = parole.map.ReticleOverlayShader(tile.size,
                    tuple(parole.conf.interface.highlight.monstersRGB))
                tile.addOverlay(overlay)
                self.highlightedTiles[tile] = overlay
            elif hasNPC and parole.conf.interface.highlight.npcs:
                overlay = parole.map.ReticleOverlayShader(tile.size,
                    tuple(parole.conf.interface.highlight.npcsRGB))
                tile.addOverlay(overlay)
                self.highlightedTiles[tile] = overlay
            elif hasItem and parole.conf.interface.highlight.items:
                overlay = parole.map.ReticleOverlayShader(tile.size,
                    tuple(parole.conf.interface.highlight.itemsRGB))
                tile.addOverlay(overlay)
                self.highlightedTiles[tile] = overlay

    def listen(self, event):
        """
        Handles:
            - "walk" ("north"/"south" + "east"/"west")
            - "enter tile" (obj, dest, map)
            - "set stat"/"increase stat"/"decrease stat"
            - "translocate"
        """
        Creature.listen(self, event)
        #parole.debug('Player got event: %r', event)
        map = None
        if self.parentTile:
            map = self.parentTile.map

        if event.id == 'start sim':
            self.acquireStartingKit()

        elif event.id == 'walk':
            assert(map)
            dx, dy = displacement(event.args[0])
            dest = self.pos[0] + dx, self.pos[1] + dy
            if dest[0] < 0 or dest[0] >= map.cols or dest[1] < 0 or \
                    dest[1] >= map.rows:
                # destination is oob; continue waiting for a command
                main.schedule.add(Event('get command', main.schedule.time))
                return

            # if there's a monster in the destination, we're going to attack
            # rather than move
            for obj in map[dest]:
                if isinstance(obj, Monster):
                    # It's assumed there can be only one monster in a tile
                    main.schedule.add(Event('get command', self.melee(obj)+1))
                    return

            if (not self.clipping) or (not self.blockedAt(map[dest])):
                # destination is not blocked; schedule translocation thereto
                time = main.schedule.time + self.timeOfAction(self.walkEnergy)
                self.doFatigue(self.walkEnergy)
                main.schedule.add(Event('translocate', time, 
                    args=(self, dest, map), dests=[self]))
                main.schedule.add(Event('get command', time + 1))
                return
            else:
                # destination is blocked; continue waiting for a command
                main.schedule.add(Event('get command', main.schedule.time))
                if not visible(dest):
                    main.transient('You feel an obstacle here.', dest)
            return
        elif event.id == 'enter tile':
            obj, dest, map = event.args
            if obj is self:
                # we've moved, so recenter the view on us
                if self.pos and not self.dead:
                    main.mapFrame.centerAtTile(self.pos)
                    main.mapFrame.selectTile(self.pos)

                # bind visibility
                if not main.mapFrame.fovObj:# and not parole.conf.options.wizard:
                    self.bindMe()


        elif event.id in ('set stat', 'increase stat', 'decrease stat'):
            creature, statname, value = event.args
            if (creature is self) and (statname in ('perception',
                    'perceptionMod')) :
                # our perception has changed, so rebind the view with new fov
                # radius
                if self.parentTile:
                    m = interface.messageBox('Patience...')
                    parole.display.update()
                    self.bindMe()
                    parole.display.scene.remove(m)

        elif event.id == 'translocate' and not event.canceled:
            import dungeon # do it here because python sucks
            obj, dest, map = event.args
            #map = self.parentTile.map
            if obj is self:
                # give a message about items underfoot
                items = []
                downShaft = False
                upShaft = False
                for x in map[dest]:
                    if isinstance(x, sim_items.Item):
                        items.append(x)
                    elif isinstance(x, dungeon.DownStair):
                        downShaft = True
                    elif isinstance(x, dungeon.UpStair):
                        upShaft = True

                if not visible(dest):
                    if downShaft:
                        if random.randint(1,5) > 1:
                            main.transient('You almost fall down a shaft in the '\
                                'ground here!', dest)
                        else:
                            # fall down the shaft
                            # TODO: shouldn't be duplicating so much code
                            main.transient('Unable to see your surroundings, '\
                                'you stumble down some sort of shaft in the '\
                                'ground!', dest)
                            nextLevel = main.currentLevel + 1
                            if nextLevel not in main.levels:
                                thelevel, ustairPos, dstairPos = \
                                    dungeon.makeLevel(nextLevel)
                                main.addLevel(nextLevel, thelevel, ustairPos,
                                        dstairPos)
                                main.originalAmbientIntensity[thelevel] = \
                                    thelevel.ambientIntensity
                            nextMap, startPos, dstairPos = main.levels[nextLevel]
                            main.schedule.listen('new map', main.MapSender(nextMap,
                                startPos))
                            main.schedule.add(Event('new map', 
                                main.schedule.time + 2000,
                                "You tumble down out of the shaft. Ouch!"))
                            # TODO: tie falling damage to carried weight
                            main.schedule.add(Event('decrease stat',
                                main.schedule.time + 2001,
                                (main.player, 'hp', random.randint(5,15)),
                                dests=[main.player]))
                            main.player.lastDamageSource = main.FallDamage()
                            return
                    if upShaft:
                        main.transient('You feel some sort of shaft leading '\
                            'up here.', dest)
                    if len(items)==1:
                        main.transient('There is an item here.', dest)
                    elif len(items) > 1:
                        main.transient('There are some items here.', dest)
                else:
                    if downShaft:
                        main.transient('There is a mineshaft leading downward '\
                                'here.', dest)
                    if upShaft:
                        main.transient('There is a mineshaft leading upward '\
                                'here', dest)
                    parole.debug('About to print there is a foo here...')
                    if len(items)==1:
                        main.transient('There is %s here.' % items[0], dest)
                    elif len(items) > 1:
                        main.transient('There are some items here.', dest)
                    parole.debug('...done')


        #parole.debug('Event ignored.')
            
